Skip to content

feat(skill): add cargento-release, which reads surfaces rather than commit prefixes - #233

Merged
gcko merged 2 commits into
mainfrom
feat/cargento-release-skill
Aug 28, 2026
Merged

feat(skill): add cargento-release, which reads surfaces rather than commit prefixes#233
gcko merged 2 commits into
mainfrom
feat/cargento-release-skill

Conversation

@gcko

@gcko gcko commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Adds cargento-release, a repository development skill that decides which version to cut from what changed since the last tag, cuts it, verifies it landed, and optionally posts release notes to Slack.

Why a skill rather than a paragraph in AGENTS.md

AGENTS.md already owns how to release: tag main, the workflow does the rest. It said nothing about which number, and that turns out to be the part with a wrong intuition sitting under it.

Measured over the whole tag history: 27 release tags, so 26 version decisions after 0.1.0. Fifteen minor, eleven patch, no major ever. Reading the bump off commit prefixes would have agreed 23 times and been wrong 3 times:

  • v0.4.3 was a patch containing a feat — listing Cargento in the shared marketplace. Packaging.
  • v0.8.1 was a patch containing a featfeat(bench), a tool for measuring a session mix you do not have. A developer benchmark is not a product surface.
  • v0.8.0 was a minor containing only a fix — to the release workflow itself. This one does not dissolve and the skill says so.

The first two stop being exceptions the moment the question is "can a user see it?" rather than "what prefix did the author use?". So the skill reads surfaces (web/, CLI flags, HTTP routes, the harness registry, payload keys, hook events, the stdio MCP tool, the Python floor, the shipped skill body) and treats the commit census as evidence rather than verdict.

Major is deliberately not inferable. Cargento is 0.x, and semver holds the major at 0 until the interface is declared stable, so 1.0.0 is a decision to commit to that interface rather than a consequence of any diff. The skill refuses to infer one and instead lists what 1.0.0 would freeze. A skill that classified majors from a changelog would be inventing an answer.

The Slack half

Gated, and in that order deliberately: ask during the release approval, then draft, then verify every claim against the commit range, then run humanizer:humanizer, then show the operator the text, then send. Verification precedes humanising because humanising a false claim only makes it read better. The channel is resolved by search at run time rather than hardcoded, since this repository is public.

Adversarial review

One reviewer, with authority to fix, found 8 defects — every one a silently-empty scan, which is the worst failure mode here because a grep that returns nothing reads as "no breaking changes". The one worth naming: the HTTP-route grep matched only the GET ladder, because POST routes are dict entries. On the range that added the ask lane it found 7 changes and missed 9, including /api/ask, /api/answer, /api/dismiss and /api/ask/withdraw. A release adding four public routes would have scanned clean. sync-docs documents that exact trap; the skill shipped the bug it warns about.

It also found $LAST unset across shell blocks (making "$LAST"..HEAD resolve to HEAD..HEAD: zero commits, empty diffs, exit 0), gh run list --limit 1 returning one of three workflows that run on every main push, and two whole surfaces missing from the scan — hook events and mcp_server.py, which landed 826 lines in v0.13.0.

Every claimed number survived re-derivation. It tested competing classification rules and found none that fits as well, and it reported the honest limit: cargento_runtime/web/ did not exist before v0.6.0, so five of the fifteen minors predate the layout the scan pins to and a full backtest of the surface rule is not available. The skill never claims one.

AGENTS.md

Two additions (the doc-ownership row, and a pointer from "Versioning and Releases" to the skill), plus one correction the review surfaced: that section claimed the Release workflow runs "behavior-focused dashboard test modules". It runs unittest discover over the whole dashboard suite.

Verification

  • validate_plugins.py green, and proven to cover the new skill: removing the Codex alias fails with missing Codex alias for canonical Claude skill.
  • The cross-document references are Markdown links rather than prose, so the validator checks them. Proven non-vacuous: renaming ## Parallel Work fails with Markdown anchor does not exist.
  • Contract: frontmatter name kebab-case matching the directory, description 210 chars with no angle brackets, short_description 38 chars (25 to 64), default_prompt contains $cargento-release, symlink relative and correctly targeted, no banned localhost spelling.
  • Full pre-PR suite green: ruff, ruff format --check, mypy --strict (108 files), lint_embedded.py (node present, JS syntax genuinely checked), 1,935 dashboard tests + 192 scripts tests OK, bump_version.py --current 0.16.0 with no manifest in the diff.

gcko and others added 2 commits August 28, 2026 08:04
Adversarial review of the new skill. The measured claims all held: 27 tags,
26 decisions, 15 minor / 11 patch / 0 major, prefix classification right 23
of 26, the three named anomalies, and zero uses of BREAKING CHANGE or the !
suffix. The command blocks did not.

- The route grep pinned on `path == "/` matched the GET ladder only. POST
  routes are dict entries (`"/api/answer": self._answer,`), so the release
  that added /api/ask, /api/answer, /api/dismiss and /api/ask/withdraw would
  have scanned clean. Widened to any quoted absolute path in http_api.py.
- `$LAST` was set in step 1 and used in step 2, but each block is its own
  shell. An empty `$LAST` makes `"$LAST"..HEAD` resolve to `HEAD..HEAD`:
  zero commits, empty diffs, exit 0, indistinguishable from a release with
  nothing in it. Every block now re-derives it and checks it is non-empty.
- The green check used `--limit 1`, which returns one of the three workflows
  that run on every main push. It reported a green Plugin Compatibility here.
  Filters on the head commit and lists all three instead.
- The scan had no command for hook events or the stdio MCP tool, both of
  which step 3 and step 4 name as surfaces. v0.13.0 added mcp_server.py,
  826 lines, invisible to the old list. Top-level payload keys live in
  aggregate.py, not sessions.py, so that pathspec gained aggregate.py too.
- "The last four commands in step 2 cover it" named the wrong four: it
  excluded CLI flags and routes, the two most break-prone surfaces, and
  included a --stat that shows no removals. Replaced with a positional
  reference that cannot drift.
- `$TAG` was used in step 1 before any version existed; the check now runs
  in step 4 where the proposal names one. `$VERSION` was never assigned in
  steps 5 and 6.
- Step 5 re-pulls main, which can move the tip past the commit step 1
  cleared. The workflow releases the tip, not the tagged commit, so the
  green check now repeats there.
- The census sed left unprefixed merge subjects intact: eight full subject
  lines for v0.7.0 rather than one count.

AGENTS.md said the Release workflow runs "behavior-focused dashboard test
modules". It runs `unittest discover` over the whole dashboard suite.
Corrected in both places.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Jared Scott <jared.scott@variable.team>
Three items from the adversarial review's left-alone list, acted on because each
is cheap and no CI cycle is being spent to get them.

The two cross-document references were plain text, matching `burndown`'s style.
That style is why nothing checked them: `validate_plugins.py` resolves links and
anchors in canonical skill bodies, and a file with no links passes that check
vacuously. Renaming `## Parallel Work` or `## Voice and tone` would have broken
the skill silently with CI still green. Both are now links, and the probe
confirms the check bites: renaming the AGENTS.md heading fails the build with
`Markdown anchor does not exist`. The divergence from `burndown` is deliberate
and is the safer direction.

AGENTS.md restated a figure the skill owns ("across 26 measured version
decisions the prefixes would have been wrong three times"). It was accurate, but
the doc-ownership rule exists to stop exactly this: a second copy nothing would
remind you to update when the history section is re-measured. It now points at
the skill's history section instead of carrying the number.

Step 6 asserted the `chore(release)` bump commit unconditionally. The workflow
skips it when the manifests already carry the tagged version, which is the
initial-release path and the resume path, so the sentence was false on two real
paths. Step 1's parity check is what makes it unconditional on a normal release,
and that is now said rather than assumed.

Left alone deliberately, with the reviewer's reasoning accepted: the plural in
"store relocation environment variables" (four are honored, so the plural is
more accurate than the singular would be), `quality-gate.yml` outside the
Python-floor scan (a CI pin moving alone is not a floor raise, and pyproject is
the declaration site), and `fail_under` riding along in that grep (a threshold
ratchet is worth seeing at release time, and it is now labelled as a CI knob
rather than as evidence of a bump).

Signed-off-by: Jared Scott <jared.scott@variable.team>
@github-actions

Copy link
Copy Markdown
Contributor

Coverage

Name                                                                  Stmts   Miss Branch BrPart  Cover
-------------------------------------------------------------------------------------------------------
cargento/skills/cargento/agy_hook.py                                     79     14     28      7  78.5%
cargento/skills/cargento/cargento_runtime/__init__.py                     0      0      0      0 100.0%
cargento/skills/cargento/cargento_runtime/aggregate.py                  212      1     64      0  99.6%
cargento/skills/cargento/cargento_runtime/asks.py                       110      0     28      0 100.0%
cargento/skills/cargento/cargento_runtime/claude_data.py                305     33    142     16  89.0%
cargento/skills/cargento/cargento_runtime/cli.py                        126     14     26      3  87.5%
cargento/skills/cargento/cargento_runtime/collectors/__init__.py          0      0      0      0 100.0%
cargento/skills/cargento/cargento_runtime/collectors/antigravity.py     410     41    166     25  87.2%
cargento/skills/cargento/cargento_runtime/collectors/claude.py          261     19     94     12  90.7%
cargento/skills/cargento/cargento_runtime/collectors/codex.py            98      7     38      7  89.7%
cargento/skills/cargento/cargento_runtime/collectors/copilot.py         148      6     50      2  96.0%
cargento/skills/cargento/cargento_runtime/collectors/cursor.py          277     20    106     17  89.8%
cargento/skills/cargento/cargento_runtime/collectors/droid.py            32      3      6      1  89.5%
cargento/skills/cargento/cargento_runtime/collectors/gemini.py           53      7     16      4  84.1%
cargento/skills/cargento/cargento_runtime/collectors/goose.py            89     11     28      4  87.2%
cargento/skills/cargento/cargento_runtime/collectors/opencode.py         78      6     26      2  92.3%
cargento/skills/cargento/cargento_runtime/collectors/pi.py              326     34    152     20  88.7%
cargento/skills/cargento/cargento_runtime/config.py                     187      1     20      1  99.0%
cargento/skills/cargento/cargento_runtime/diagnostics.py                 84      4     26      4  92.7%
cargento/skills/cargento/cargento_runtime/dismissals.py                 113      2     28      2  97.2%
cargento/skills/cargento/cargento_runtime/events.py                     162      0     62      0 100.0%
cargento/skills/cargento/cargento_runtime/http_api.py                   515     31    176      9  94.2%
cargento/skills/cargento/cargento_runtime/io.py                         126      2     28      0  98.7%
cargento/skills/cargento/cargento_runtime/lifecycle.py                  323     15    104      6  95.1%
cargento/skills/cargento/cargento_runtime/notifications.py              174     14     60      4  91.5%
cargento/skills/cargento/cargento_runtime/observation.py                235      2     64      0  99.3%
cargento/skills/cargento/cargento_runtime/observer.py                   249     34    110     13  84.7%
cargento/skills/cargento/cargento_runtime/probe.py                       44      0     18      1  98.4%
cargento/skills/cargento/cargento_runtime/quota.py                      333      2    112      1  99.3%
cargento/skills/cargento/cargento_runtime/records.py                    256      5    114      9  96.2%
cargento/skills/cargento/cargento_runtime/sessions.py                   101      0     44      0 100.0%
cargento/skills/cargento/cargento_runtime/snapshot.py                    36      0      4      0 100.0%
cargento/skills/cargento/cargento_runtime/spacedock.py                  397     46    200     24  87.9%
cargento/skills/cargento/cargento_runtime/state.py                       64      0      2      0 100.0%
cargento/skills/cargento/cargento_runtime/stream.py                      57      0      8      0 100.0%
cargento/skills/cargento/cargento_runtime/transcripts.py                361     16    192     15  94.4%
cargento/skills/cargento/cargento_runtime/turns.py                      197     14    104     12  90.7%
cargento/skills/cargento/cargento_runtime/web/__init__.py                 0      0      0      0 100.0%
cargento/skills/cargento/cargento_runtime/web/page.py                    54      0     14      0 100.0%
cargento/skills/cargento/event_hook.py                                   86      4     28      3  93.9%
cargento/skills/cargento/mcp_server.py                                  377     22    112     14  92.2%
cargento/skills/cargento/notify_hook.py                                  49     15      6      1  67.3%
cargento/skills/cargento/server.py                                        3      0      2      1  80.0%
cargento/skills/cargento/statusline_hook.py                             131     13     46      8  87.0%
scripts/bench_collect.py                                                211     10     54      6  94.0%
scripts/bench_event_latency.py                                           67     67     14      0   0.0%
scripts/bump_version.py                                                  60     12     24      5  77.4%
scripts/capture_hook.py                                                 287     30     86     11  88.5%
scripts/derive_prompt_shapes.py                                         210     16     88     14  89.3%
scripts/lint_embedded.py                                                 92      3     28      2  95.8%
scripts/validate_plugins.py                                             663    186    386     58  69.3%
-------------------------------------------------------------------------------------------------------
TOTAL                                                                  8908    782   3334    344  89.7%

Threshold: fail_under in pyproject.toml · label coverage-exception to bypass (visible in PR timeline).

@gcko
gcko merged commit 411c9ee into main Aug 28, 2026
12 checks passed
@gcko
gcko deleted the feat/cargento-release-skill branch August 28, 2026 00:22
gcko added a commit that referenced this pull request Aug 28, 2026
…sync marker (#234)

Two loose ends from cutting v0.17.0, the first real run of `cargento-release`.

Step 5 said `git tag "v$VERSION"`. On a machine with `tag.gpgsign true` that tries
to create a signed tag, finds no message, and dies with `no tag message?` before
anything reaches the remote. It failed on the release it was written for.

The fix is `git -c tag.gpgsign=false`, which forces the lightweight form. That is
what this repository's release tags are (`git cat-file -t v0.16.0` prints
`commit`, not `tag`) and it is what the workflow's own `git tag -f` re-creates
when it moves the tag onto the bump commit, so the override matches the end state
rather than working around it. Where signing is off it does nothing. A
`git cat-file -t` check rides along because the wrong outcome is silent in the
other direction: an annotated tag pushes fine and the mismatch only shows up
later.

Rehearsed this time rather than reasoned about: created a throwaway tag locally,
confirmed `commit`, deleted it, and confirmed the bare form still fails. The
comment in the skill records why that rehearsal is possible and why the original
command was the one that went untested.

Also stamps `docs-synced-through` from `0f9d086` to `ec10918`, per AGENTS.md's
rule to advance it once from main after merges rather than per branch. The range
covers four commits: the previous stamp (#231), the sessions-table title label
(#232), the `cargento-release` skill itself (#233), and the v0.17.0 release bump.

Signed-off-by: Jared Scott <jared.scott@variable.team>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant